#!/usr/bin/env python
# -*- mode: python; coding: utf-8; -*-
##---------------------------------------------------------------------------##
##
## Copyright (C) 1998-2003 Markus Oberhumer <markus@oberhumer.com>
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
## Copyright (c) 2012 Shlomi Fish shlomif@shlomifish.org
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##--------------------------------------------------------------------------##
"""
Random generator objects for consistent shuffling of a card/tile pack.
"""
__docformat__ = 'restructuredtext en'
#_ = lambda n: n

import random
import logging

LOGGER = logging.getLogger('NPS.GameLib.PackMixin')


class LCRandom(object):
    """
    Linear Congruential random generator[#]_.

    Used for seed within first 32 bits (32767) for FreeCell compatibility.

    .. [#] http://rosettacode.org/wiki/Linear_congruential_generator
    """
    __seed = 0
    __initseed = 0

    def __init__(self, seed=None):
        """
        Initialize random value generator.

        .. \__init__(seed:long=None) -> None

        *Paremeters:*
          ``seed`` : {number}
            Value for initial random state.

          ``origin`` : {text}
            Flag to indicate how seed was chosen.

        *Requires*:
          Methods:
            ``LCRandom.setstate``; ``LCRandom.getstate``;

          Variables:
            ``LCRandom.__initseed`` - Seed creating intial random state.
        """
        self.setstate(seed)
        self.__initseed = self.getstate()

    def __getstate__(self):
        """
        Magic method for pickling random class that return ``inital_seed``.

        *Requires*:
          Variables:
            ``LCRandom.__initseed`` - Seed creating intial random state.
        """
        return self.__initseed

    def __setstate__(self, seed):
        """
        Magic method for unpickling random class that sets ``seed`` with given.

        *Requires*:
          Variables:
            ``LCRandom.__seed`` - Current seed creating next random state.
        """
        self.__seed = seed

    def __repr__(self):
        """
        Magic method for representing random class that returns initial seed
        padded with 0 for consistent 10 digits.

        *Requires*:
          Variables:
            ``LCRandom.__initseed`` - Seed creating intial random state.
        """
        return '{0:010}'.format(self.__initseed)

    @property
    def __random_value(self):
        """
        Sets and returns current random value from prior ``seed`` value bit
        shift right by 2 bytes.

        .. \__random_value -> long

        *Requires*:
          Variables:
            ``LCRandom.__seed``.
        """
        self.__seed = (self.__seed * 214013L + 2531011L) & 0x7fffffffL
        return self.__seed >> 16

    def random(self):
        """
        Returns random value.

        .. random() -> float

        *Requires*:
          Variables:
            ``LCRandom.__random_value``.

        >>> r_class = LCRandom(1)
        >>> r_class.random()
        0.001251220703125
        >>> r_class.random()
        0.563568115234375
        >>> r_class.random()
        0.19329833984375
        """
        return (self.__random_value) / 32768.0

    def randint(self, int1, int2):
        """
        Returns random value between range given include both ends.

        .. randint(int1:int, int2:int) -> int

        *Requires*:
          Variables:
            ``LCRandom.__random_value``.

        >>> r_class = LCRandom(1)
        >>> r_class.randint(0, 51)
        41
        >>> r_class.randint(0, 50)
        5
        >>> r_class.randint(0, 49)
        34
        """
        return int1 + (int(self.__random_value) % (int(int2) + 1 - int(int1)))

    def choice(self, sequence):
        """
        Returns random choice in sequence given.

        .. choice(sequence:seq) -> int

        *Requires*:
          Methods:
            ``LCRandom.random``.
        """
        return sequence[int(self.random() * len(sequence))]

    def randrange(self, int1, int2):
        """
        Returns random value between range given include only start.

        .. randint(int1:int, int2:int) -> int

        *Requires*:
          Methods:
            ``LCRandom.randint``.

        >>> r_class = LCRandom(1)
        """
        return self.randint(int1, int2 - 1)

    def shuffle(self, sequence):
        """
        Returns sequence shuffled.

        .. shuffle(sequence:seq) -> seq

        *Requires*:
          Methods:
            ``LCRandom.randint``.

        *Raises:
          ``AssertionError``:
            When sequence lacks keys for deck, rank or suit.

        >>> from collections import namedtuple
        >>>
        >>> pack = []
        >>> def build(pack):
        ...     for suit in range(4):
        ...         for rank in range(13):
        ...             pack.append({'deck': 1, 'rank': rank, 'suit': suit})
        ...
        >>> build(pack)
        >>> r_class = LCRandom(1)
        >>> r_class.shuffle(pack)
        >>> card = namedtuple('Card', ('deck', 'rank', 'suit'))
        >>> card(**pack[51]) == (1, 10, 3)  #JD
        True
        >>> card(**pack[50]) == (1, 1, 3)  #2D
        True
        >>> card(**pack[49]) == (1, 8, 2)  #9H
        True
        >>> card(**pack[48]) == (1, 10, 0)  #JC
        True
        >>> card(**pack[47]) == (1, 4, 3)  #5D
        True
        >>> card(**pack[46]) == (1, 6, 2)  #7H
        True
        >>> card(**pack[45]) == (1, 6, 0)  #7C
        True
        >>> card(**pack[44]) == (1, 4, 2)  #5H
        True
        """
        assert all(v in sequence for v in ['deck', 'rank', 'suit'])

        # Sort cards by rank and suit order of Club, Diamond, Heart, Spade
        sequence.sort(key=lambda c: (c['deck'], c['rank'],
                            {0: 0, 1: 3, 2: 2, 3: 1}.get(c['suit'], c['suit'])))

        for f_pos in range(len(sequence) - 1, -1, -1):
            t_pos = self.randint(0, f_pos)
            sequence[f_pos], sequence[t_pos] = sequence[t_pos], sequence[f_pos]

    @property
    def _nextstate(self):
        """
        Returns next increment of initial random state value .

        .. \_nextstate -> long

        *Requires*:
          Methods:
            ``LCRandom.__initseed``.
        """
        return self.__initseed + 1L

    @property
    def _initstate(self):
        """
        Returns intial random state value increment.

        .. \_initstate -> long

        *Requires*:
          Methods:
            ``LCRandom.__initseed``.
        """
        return self.__initseed

    def reset(self):
        """
        Resets intial random state value.

        .. reset() -> None

        *Requires*:
          Methods:
            ``LCRandom.__seed``; ``LC31Random.__initseed``.
        """
        self.__seed = self.__initseed

    def getstate(self):
        """
        Returns current random state value. Mirrors included python
        ``random.Random``.

        .. getstate() -> int

        *Requires*:
          Variables:
            ``LCRandom.seed``.
        """
        return self.__seed

    def setstate(self, seed):
        """
        Set given seed as current random state. When seed is ``None``, generate
        new seed using basic time method.

        .. setstate(seed)

        *Requires*:
          Variables:
            ``LCRandom.__seed`` - Set new seed value.

        *Raises*:
          ``ValueError``:
            When seed not in range 0 to 2147483648 (32 bits).
        """
        import time
        if seed is None:
            seed = long(time.time() * 256.0)
            seed = (seed ^ (seed >> 24)) % (0x7fffffffL + 1L)
        seed = long(seed)
        if not (0L <= seed <= 0x7fffffffL):
            raise ValueError('seed out of range')
        self.__seed = seed


class MTRandom(random.Random):
    """
    Mersenne Twister random number generator.

    Use for seeds above 32 bits (32767). Wraps standard python random module
    with additional methods for card games. Store original state. Increment
    next seed value. Restore original state.
    """
    __initseed = 0

    def __init__(self, seed=None):
        """
        Initialize random value generator.

        .. \__init__(seed:long=None) -> None

        *Paremeters:*
          ``seed`` : {number}
            Seed for initial random state.

        *Requires*:
          Variables:
            ``MTRandom.__initseed`` - Seed creating intial random state.
        """
        super(MTRandom, self).__init__(seed)
        self.__initseed = seed or self.__initseed

    def reset(self):
        """
        Resets intial random state value.

        .. reset() -> None

        *Requires*:
          Methods:
            ``MTRandom.seed``.
          Variables:
            ``MTRandom.__initseed``.
        """
        self.seed(self.__initseed)

    def __repr__(self):
        """
        Magic method for representing random class that returns initial seed
        padded with 0 for consistent 20 digits.

        *Requires*:
          Variables:
            ``MTRandom.__initseed``.
        """
        return '{0:020}'.format(self.__initseed)

    @property
    def _nextstate(self):
        """
        Returns next increment of initial random state value .

        .. \_nextstate -> long

        *Requires*:
          Methods:
            ``MTRandom.__initseed``.
        """
        return self.__initseed + 1L

    @property
    def _initstate(self):
        """
        Returns intial random state value increment.

        .. \_initstate -> long

        *Requires*:
          Methods:
            ``MTRandom.__initseed``.
        """
        return self.__initseed


def random_class(seed=None):
    """
    Returns new random class from given ``seed``.

    Creates a Mersenne Twister generator for all given ``seed`` values above
    ``1 << 31``. Any value below creates a Linear Congruential generator, to
    create shuffle orders compatible with MS FreeCell game numbers.

    .. random_class(seed:long=*randint*) -> None

    *Parameters*:
      ``seed`` : {text|None}
        Value to initialize the new random class.

    *Requires*:
      Methods:
        ``random.randrange``, ``LCRandom``, ``MTRandom``.

    >>> r_class = random_class()
    >>> type(r_class) == MTRandom
    True
    >>> r_class = random_class(35)
    >>> type(r_class) == LCRandom
    True
    >>> r_class = random_class(10 ** 18)
    >>> type(random_class(1 << 32)) == MTRandom
    True
    """
    if seed is None:
        # Create at least 17 digits
        seed = random.randrange(10 ** 17, 10 ** 20)
    else:
        import re
        seed = long(re.sub('\D', '', str(seed)))  # Strip all but digits.

    if seed:
        rndm_class = LCRandom if 0 <= seed <= (1 << 31) else MTRandom
        return rndm_class(long(seed))


def update_random_state(rndm_class=None):
    """
    Update random class state for first shuffle.

    Verify ``random_class`` is either ``LCRandom`` or ``MTRandom``. When not
    either type, call ``random_class`` method to create a new class. Reset final
    random class to its initial state.

    .. update_random_state(random_class:Random) -> None

    *Parameters*:
      ``random_class`` : {MTRandom|LCRandom} MTRandom
        Set random class generator to shuffle card pack for game.

    >>> isinstance(update_random_state(), MTRandom)
    True
    >>> isinstance(update_random_state(LCRandom()), LCRandom)
    True
    >>> isinstance(update_random_state(MTRandom()), MTRandom)
    True
    """
    if not isinstance(rndm_class, (LCRandom, MTRandom,)):
        rndm_class = random_class()
    rndm_class.reset()  # reset to initial seed
    LOGGER.debug('%s %s', rndm_class.__class__.__name__, rndm_class)
    return rndm_class


if __name__ == '__main__':
    import doctest
    doctest.testmod()